import {
BufferAttribute,
    BufferGeometry,
    Color,
    FileLoader,
    Group,
    LineBasicMaterial,
    LineSegments,
    Loader,
    Matrix4,
    Mesh,
    MeshStandardMaterial,
    ShaderMaterial,
    SRGBColorSpace,
    UniformsLib,
    UniformsUtils,
    Vector3,
    Ray
  } from 'three';
  
  // Special surface finish tag types.
  // Note: "MATERIAL" tag (e.g. GLITTER, SPECKLE) is not implemented
  const FINISH_TYPE_DEFAULT = 0;
  const FINISH_TYPE_CHROME = 1;
  const FINISH_TYPE_PEARLESCENT = 2;
  const FINISH_TYPE_RUBBER = 3;
  const FINISH_TYPE_MATTE_METALLIC = 4;
  const FINISH_TYPE_METAL = 5;
  
  // State machine to search a subobject path.
  // The LDraw standard establishes these various possible subfolders.
  const FILE_LOCATION_TRY_PARTS = 0;
  const FILE_LOCATION_TRY_P = 1;
  const FILE_LOCATION_TRY_MODELS = 2;
  const FILE_LOCATION_AS_IS = 3;
  const FILE_LOCATION_TRY_RELATIVE = 4;
  const FILE_LOCATION_TRY_ABSOLUTE = 5;
  const FILE_LOCATION_NOT_FOUND = 6;
  
  const MAIN_COLOUR_CODE = '16';
  const MAIN_EDGE_COLOUR_CODE = '24';
  
  const COLOR_SPACE_LDRAW = SRGBColorSpace;
  
  const _tempVec0 = new Vector3();
  const _tempVec1 = new Vector3();
  
  class LDrawConditionalLineMaterial extends ShaderMaterial {
  
    constructor( parameters ) {
  
      super( {
  
        uniforms: UniformsUtils.merge( [
          UniformsLib.fog,
          {
            diffuse: {
              value: new Color()
            },
            opacity: {
              value: 1.0
            }
          }
        ] ),
  
        vertexShader: /* glsl */`
          attribute vec3 control0;
          attribute vec3 control1;
          attribute vec3 direction;
          varying float discardFlag;
  
          #include <common>
          #include <color_pars_vertex>
          #include <fog_pars_vertex>
          #include <logdepthbuf_pars_vertex>
          #include <clipping_planes_pars_vertex>
          void main() {
            #include <color_vertex>
  
            vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
            gl_Position = projectionMatrix * mvPosition;
  
            // Transform the line segment ends and control points into camera clip space
            vec4 c0 = projectionMatrix * modelViewMatrix * vec4( control0, 1.0 );
            vec4 c1 = projectionMatrix * modelViewMatrix * vec4( control1, 1.0 );
            vec4 p0 = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
            vec4 p1 = projectionMatrix * modelViewMatrix * vec4( position + direction, 1.0 );
  
            c0.xy /= c0.w;
            c1.xy /= c1.w;
            p0.xy /= p0.w;
            p1.xy /= p1.w;
  
            // Get the direction of the segment and an orthogonal vector
            vec2 dir = p1.xy - p0.xy;
            vec2 norm = vec2( -dir.y, dir.x );
  
            // Get control point directions from the line
            vec2 c0dir = c0.xy - p1.xy;
            vec2 c1dir = c1.xy - p1.xy;
  
            // If the vectors to the controls points are pointed in different directions away
            // from the line segment then the line should not be drawn.
            float d0 = dot( normalize( norm ), normalize( c0dir ) );
            float d1 = dot( normalize( norm ), normalize( c1dir ) );
            discardFlag = float( sign( d0 ) != sign( d1 ) );
  
            #include <logdepthbuf_vertex>
            #include <clipping_planes_vertex>
            #include <fog_vertex>
          }
        `,
  
        fragmentShader: /* glsl */`
        uniform vec3 diffuse;
        uniform float opacity;
        varying float discardFlag;
  
        #include <common>
        #include <color_pars_fragment>
        #include <fog_pars_fragment>
        #include <logdepthbuf_pars_fragment>
        #include <clipping_planes_pars_fragment>
        void main() {
  
          if ( discardFlag > 0.5 ) discard;
  
          #include <clipping_planes_fragment>
          vec3 outgoingLight = vec3( 0.0 );
          vec4 diffuseColor = vec4( diffuse, opacity );
          #include <logdepthbuf_fragment>
          #include <color_fragment>
          outgoingLight = diffuseColor.rgb; // simple shader
          gl_FragColor = vec4( outgoingLight, diffuseColor.a );
          #include <tonemapping_fragment>
          #include <colorspace_fragment>
          #include <fog_fragment>
          #include <premultiplied_alpha_fragment>
        }
        `,
  
      } );
  
      Object.defineProperties( this, {
  
        opacity: {
          get: function () {
  
            return this.uniforms.opacity.value;
  
          },
  
          set: function ( value ) {
  
            this.uniforms.opacity.value = value;
  
          }
        },
  
        color: {
          get: function () {
  
            return this.uniforms.diffuse.value;
  
          }
        }
  
      } );
  
      this.setValues( parameters );
      this.isLDrawConditionalLineMaterial = true;
  
    }
  
  }
  
  class ConditionalLineSegments extends LineSegments {
  
    constructor( geometry, material ) {
  
      super( geometry, material );
      this.isConditionalLine = true;
  
    }
  
  }
  
  function generateFaceNormals( faces ) {
  
    for ( let i = 0, l = faces.length; i < l; i ++ ) {
  
      const face = faces[ i ];
      const vertices = face.vertices;
      const v0 = vertices[ 0 ];
      const v1 = vertices[ 1 ];
      const v2 = vertices[ 2 ];
  
      _tempVec0.subVectors( v1, v0 );
      _tempVec1.subVectors( v2, v1 );
      face.faceNormal = new Vector3()
        .crossVectors( _tempVec0, _tempVec1 )
        .normalize();
  
    }
  
  }
  
  const _ray = new Ray();
  function smoothNormals( faces, lineSegments, checkSubSegments = false ) {
  
    // NOTE: 1e2 is pretty coarse but was chosen to quantize the resulting value because
    // it allows edges to be smoothed as expected (see minifig arms).
    // --
    // And the vector values are initialize multiplied by 1 + 1e-10 to account for floating
    // point errors on vertices along quantization boundaries. Ie after matrix multiplication
    // vertices that should be merged might be set to "1.7" and "1.6999..." meaning they won't
    // get merged. This added epsilon attempts to push these error values to the same quantized
    // value for the sake of hashing. See "AT-ST mini" dishes. See mrdoob/three#23169.
  
    const hashMultiplier = ( 1 + 1e-10 ) * 1e2;
    function hashVertex( v ) {
  
      const x = ~ ~ ( v.x * hashMultiplier );
      const y = ~ ~ ( v.y * hashMultiplier );
      const z = ~ ~ ( v.z * hashMultiplier );
  
      return `${ x },${ y },${ z }`;
  
    }
  
    function hashEdge( v0, v1 ) {
  
      return `${ hashVertex( v0 ) }_${ hashVertex( v1 ) }`;
  
    }
  
    // converts the two vertices to a ray with a normalized direction and origin of 0, 0, 0 projected
    // onto the original line.
    function toNormalizedRay( v0, v1, targetRay ) {
  
      targetRay.direction.subVectors( v1, v0 ).normalize();
  
      const scalar = v0.dot( targetRay.direction );
      targetRay.origin.copy( v0 ).addScaledVector( targetRay.direction, - scalar );
  
      return targetRay;
  
    }
  
    function hashRay( ray ) {
  
      return hashEdge( ray.origin, ray.direction );
  
    }
  
    const hardEdges = new Set();
    const hardEdgeRays = new Map();
    const halfEdgeList = {};
    const normals = [];
  
    // Save the list of hard edges by hash
    for ( let i = 0, l = lineSegments.length; i < l; i ++ ) {
  
      const ls = lineSegments[ i ];
      const vertices = ls.vertices;
      const v0 = vertices[ 0 ];
      const v1 = vertices[ 1 ];
      hardEdges.add( hashEdge( v0, v1 ) );
      hardEdges.add( hashEdge( v1, v0 ) );
  
      // only generate the hard edge ray map if we're checking subsegments because it's more expensive to check
      // and requires more memory.
      if ( checkSubSegments ) {
  
        // add both ray directions to the map
        const ray = toNormalizedRay( v0, v1, new Ray() );
        const rh1 = hashRay( ray );
        if ( ! hardEdgeRays.has( rh1 ) ) {
  
          toNormalizedRay( v1, v0, ray );
          const rh2 = hashRay( ray );
  
          const info = {
            ray,
            distances: [],
          };
  
          hardEdgeRays.set( rh1, info );
          hardEdgeRays.set( rh2, info );
  
        }
  
        // store both segments ends in min, max order in the distances array to check if a face edge is a
        // subsegment later.
        const info = hardEdgeRays.get( rh1 );
        let d0 = info.ray.direction.dot( v0 );
        let d1 = info.ray.direction.dot( v1 );
        if ( d0 > d1 ) {
  
          [ d0, d1 ] = [ d1, d0 ];
  
        }
  
        info.distances.push( d0, d1 );
  
      }
  
    }
  
    // track the half edges associated with each triangle
    for ( let i = 0, l = faces.length; i < l; i ++ ) {
  
      const tri = faces[ i ];
      const vertices = tri.vertices;
      const vertCount = vertices.length;
      for ( let i2 = 0; i2 < vertCount; i2 ++ ) {
  
        const index = i2;
        const next = ( i2 + 1 ) % vertCount;
        const v0 = vertices[ index ];
        const v1 = vertices[ next ];
        const hash = hashEdge( v0, v1 );
  
        // don't add the triangle if the edge is supposed to be hard
        if ( hardEdges.has( hash ) ) {
  
          continue;
  
        }
  
        // if checking subsegments then check to see if this edge lies on a hard edge ray and whether its within any ray bounds
        if ( checkSubSegments ) {
  
          toNormalizedRay( v0, v1, _ray );
  
          const rayHash = hashRay( _ray );
          if ( hardEdgeRays.has( rayHash ) ) {
  
            const info = hardEdgeRays.get( rayHash );
            const { ray, distances } = info;
            let d0 = ray.direction.dot( v0 );
            let d1 = ray.direction.dot( v1 );
  
            if ( d0 > d1 ) {
  
              [ d0, d1 ] = [ d1, d0 ];
  
            }
  
            // return early if the face edge is found to be a subsegment of a line edge meaning the edge will have "hard" normals
            let found = false;
            for ( let i = 0, l = distances.length; i < l; i += 2 ) {
  
              if ( d0 >= distances[ i ] && d1 <= distances[ i + 1 ] ) {
  
                found = true;
                break;
  
              }
  
            }
  
            if ( found ) {
  
              continue;
  
            }
  
          }
  
        }
  
        const info = {
          index: index,
          tri: tri
        };
        halfEdgeList[ hash ] = info;
  
      }
  
    }
  
    // Iterate until we've tried to connect all faces to share normals
    while ( true ) {
  
      // Stop if there are no more faces left
      let halfEdge = null;
      for ( const key in halfEdgeList ) {
  
        halfEdge = halfEdgeList[ key ];
        break;
  
      }
  
      if ( halfEdge === null ) {
  
        break;
  
      }
  
      // Exhaustively find all connected faces
      const queue = [ halfEdge ];
      while ( queue.length > 0 ) {
  
        // initialize all vertex normals in this triangle
        const tri = queue.pop().tri;
        const vertices = tri.vertices;
        const vertNormals = tri.normals;
        const faceNormal = tri.faceNormal;
  
        // Check if any edge is connected to another triangle edge
        const vertCount = vertices.length;
        for ( let i2 = 0; i2 < vertCount; i2 ++ ) {
  
          const index = i2;
          const next = ( i2 + 1 ) % vertCount;
          const v0 = vertices[ index ];
          const v1 = vertices[ next ];
  
          // delete this triangle from the list so it won't be found again
          const hash = hashEdge( v0, v1 );
          delete halfEdgeList[ hash ];
  
          const reverseHash = hashEdge( v1, v0 );
          const otherInfo = halfEdgeList[ reverseHash ];
          if ( otherInfo ) {
  
            const otherTri = otherInfo.tri;
            const otherIndex = otherInfo.index;
            const otherNormals = otherTri.normals;
            const otherVertCount = otherNormals.length;
            const otherFaceNormal = otherTri.faceNormal;
  
            // NOTE: If the angle between faces is > 67.5 degrees then assume it's
            // hard edge. There are some cases where the line segments do not line up exactly
            // with or span multiple triangle edges (see Lunar Vehicle wheels).
            if ( Math.abs( otherTri.faceNormal.dot( tri.faceNormal ) ) < 0.25 ) {
  
              continue;
  
            }
  
            // if this triangle has already been traversed then it won't be in
            // the halfEdgeList. If it has not then add it to the queue and delete
            // it so it won't be found again.
            if ( reverseHash in halfEdgeList ) {
  
              queue.push( otherInfo );
              delete halfEdgeList[ reverseHash ];
  
            }
  
            // share the first normal
            const otherNext = ( otherIndex + 1 ) % otherVertCount;
            if (
              vertNormals[ index ] && otherNormals[ otherNext ] &&
              vertNormals[ index ] !== otherNormals[ otherNext ]
            ) {
  
              otherNormals[ otherNext ].norm.add( vertNormals[ index ].norm );
              vertNormals[ index ].norm = otherNormals[ otherNext ].norm;
  
            }
  
            let sharedNormal1 = vertNormals[ index ] || otherNormals[ otherNext ];
            if ( sharedNormal1 === null ) {
  
              // it's possible to encounter an edge of a triangle that has already been traversed meaning
              // both edges already have different normals defined and shared. To work around this we create
              // a wrapper object so when those edges are merged the normals can be updated everywhere.
              sharedNormal1 = { norm: new Vector3() };
              normals.push( sharedNormal1.norm );
  
            }
  
            if ( vertNormals[ index ] === null ) {
  
              vertNormals[ index ] = sharedNormal1;
              sharedNormal1.norm.add( faceNormal );
  
            }
  
            if ( otherNormals[ otherNext ] === null ) {
  
              otherNormals[ otherNext ] = sharedNormal1;
              sharedNormal1.norm.add( otherFaceNormal );
  
            }
  
            // share the second normal
            if (
              vertNormals[ next ] && otherNormals[ otherIndex ] &&
              vertNormals[ next ] !== otherNormals[ otherIndex ]
            ) {
  
              otherNormals[ otherIndex ].norm.add( vertNormals[ next ].norm );
              vertNormals[ next ].norm = otherNormals[ otherIndex ].norm;
  
            }
  
            let sharedNormal2 = vertNormals[ next ] || otherNormals[ otherIndex ];
            if ( sharedNormal2 === null ) {
  
              sharedNormal2 = { norm: new Vector3() };
              normals.push( sharedNormal2.norm );
  
            }
  
            if ( vertNormals[ next ] === null ) {
  
              vertNormals[ next ] = sharedNormal2;
              sharedNormal2.norm.add( faceNormal );
  
            }
  
            if ( otherNormals[ otherIndex ] === null ) {
  
              otherNormals[ otherIndex ] = sharedNormal2;
              sharedNormal2.norm.add( otherFaceNormal );
  
            }
  
          }
  
        }
  
      }
  
    }
  
    // The normals of each face have been added up so now we average them by normalizing the vector.
    for ( let i = 0, l = normals.length; i < l; i ++ ) {
  
      normals[ i ].normalize();
  
    }
  
  }
  
  function isPartType( type ) {
  
    return type === 'Part' || type === 'Unofficial_Part';
  
  }
  
  function isPrimitiveType( type ) {
  
    return /primitive/i.test( type ) || type === 'Subpart';
  
  }
  
  class LineParser {
  
    constructor( line, lineNumber ) {
  
      this.line = line;
      this.lineLength = line.length;
      this.currentCharIndex = 0;
      this.currentChar = ' ';
      this.lineNumber = lineNumber;
  
    }
  
    seekNonSpace() {
  
      while ( this.currentCharIndex < this.lineLength ) {
  
        this.currentChar = this.line.charAt( this.currentCharIndex );
  
        if ( this.currentChar !== ' ' && this.currentChar !== '\t' ) {
  
          return;
  
        }
  
        this.currentCharIndex ++;
  
      }
  
    }
  
    getToken() {
  
      const pos0 = this.currentCharIndex ++;
  
      // Seek space
      while ( this.currentCharIndex < this.lineLength ) {
  
        this.currentChar = this.line.charAt( this.currentCharIndex );
  
        if ( this.currentChar === ' ' || this.currentChar === '\t' ) {
  
          break;
  
        }
  
        this.currentCharIndex ++;
  
      }
  
      const pos1 = this.currentCharIndex;
  
      this.seekNonSpace();
  
      return this.line.substring( pos0, pos1 );
  
    }
  
    getVector() {
  
      return new Vector3( parseFloat( this.getToken() ), parseFloat( this.getToken() ), parseFloat( this.getToken() ) );
  
    }
  
    getRemainingString() {
  
      return this.line.substring( this.currentCharIndex, this.lineLength );
  
    }
  
    isAtTheEnd() {
  
      return this.currentCharIndex >= this.lineLength;
  
    }
  
    setToEnd() {
  
      this.currentCharIndex = this.lineLength;
  
    }
  
    getLineNumberString() {
  
      return this.lineNumber >= 0 ? ' at line ' + this.lineNumber : '';
  
    }
  
  }
  
  // Fetches and parses an intermediate representation of LDraw parts files.
  class LDrawParsedCache {
  
    constructor( loader ) {
  
      this.loader = loader;
      this._cache = {};
  
    }
  
    cloneResult( original ) {
  
      const result = {};
  
      // vertices are transformed and normals computed before being converted to geometry
      // so these pieces must be cloned.
      result.faces = original.faces.map( face => {
  
        return {
          colorCode: face.colorCode,
          material: face.material,
          vertices: face.vertices.map( v => v.clone() ),
          normals: face.normals.map( () => null ),
          faceNormal: null
        };
  
      } );
  
      result.conditionalSegments = original.conditionalSegments.map( face => {
  
        return {
          colorCode: face.colorCode,
          material: face.material,
          vertices: face.vertices.map( v => v.clone() ),
          controlPoints: face.controlPoints.map( v => v.clone() )
        };
  
      } );
  
      result.lineSegments = original.lineSegments.map( face => {
  
        return {
          colorCode: face.colorCode,
          material: face.material,
          vertices: face.vertices.map( v => v.clone() )
        };
  
      } );
  
      // none if this is subsequently modified
      result.type = original.type;
      result.category = original.category;
      result.keywords = original.keywords;
      result.author = original.author;
      result.subobjects = original.subobjects;
      result.fileName = original.fileName;
      result.totalFaces = original.totalFaces;
      result.startingBuildingStep = original.startingBuildingStep;
      result.materials = original.materials;
      result.group = null;
      return result;
  
    }
  
    async fetchData( fileName ) {
  
      let triedLowerCase = false;
      let locationState = FILE_LOCATION_TRY_PARTS;
      while ( locationState !== FILE_LOCATION_NOT_FOUND ) {
  
        let subobjectURL = fileName;
        switch ( locationState ) {
  
          case FILE_LOCATION_AS_IS:
            locationState = locationState + 1;
            break;
  
          case FILE_LOCATION_TRY_PARTS:
            subobjectURL = 'parts/' + subobjectURL;
            locationState = locationState + 1;
            break;
  
          case FILE_LOCATION_TRY_P:
            subobjectURL = 'p/' + subobjectURL;
            locationState = locationState + 1;
            break;
  
          case FILE_LOCATION_TRY_MODELS:
            subobjectURL = 'models/' + subobjectURL;
            locationState = locationState + 1;
            break;
  
          case FILE_LOCATION_TRY_RELATIVE:
            subobjectURL = fileName.substring( 0, fileName.lastIndexOf( '/' ) + 1 ) + subobjectURL;
            locationState = locationState + 1;
            break;
  
          case FILE_LOCATION_TRY_ABSOLUTE:
  
            if ( triedLowerCase ) {
  
              // Try absolute path
              locationState = FILE_LOCATION_NOT_FOUND;
  
            } else {
  
              // Next attempt is lower case
              fileName = fileName.toLowerCase();
              subobjectURL = fileName;
              triedLowerCase = true;
              locationState = FILE_LOCATION_TRY_PARTS;
  
            }
  
            break;
  
        }
  
        const loader = this.loader;
        const fileLoader = new FileLoader( loader.manager );
        fileLoader.setPath( loader.partsLibraryPath );
        fileLoader.setRequestHeader( loader.requestHeader );
        fileLoader.setWithCredentials( loader.withCredentials );
  
        try {
  
          const text = await fileLoader.loadAsync( subobjectURL );
          return text;
  
        } catch ( _ ) {
  
          continue;
  
        }
  
      }
  
      throw new Error( 'LDrawLoader: Subobject "' + fileName + '" could not be loaded.' );
  
    }
  
    parse( text, fileName = null ) {
  
      const loader = this.loader;
  
      // final results
      const faces = [];
      const lineSegments = [];
      const conditionalSegments = [];
      const subobjects = [];
      const materials = {};
  
      const getLocalMaterial = colorCode => {
  
        return materials[ colorCode ] || null;
  
      };
  
      let type = 'Model';
      let category = null;
      let keywords = null;
      let author = null;
      let totalFaces = 0;
  
      // split into lines
      if ( text.indexOf( '\r\n' ) !== - 1 ) {
  
        // This is faster than String.split with regex that splits on both
        text = text.replace( /\r\n/g, '\n' );
  
      }
  
      const lines = text.split( '\n' );
      const numLines = lines.length;
  
      let parsingEmbeddedFiles = false;
      let currentEmbeddedFileName = null;
      let currentEmbeddedText = null;
  
      let bfcCertified = false;
      let bfcCCW = true;
      let bfcInverted = false;
      let bfcCull = true;
  
      let startingBuildingStep = false;
  
      // Parse all line commands
      for ( let lineIndex = 0; lineIndex < numLines; lineIndex ++ ) {
  
        const line = lines[ lineIndex ];
  
        if ( line.length === 0 ) continue;
  
        if ( parsingEmbeddedFiles ) {
  
          if ( line.startsWith( '0 FILE ' ) ) {
  
            // Save previous embedded file in the cache
            this.setData( currentEmbeddedFileName, currentEmbeddedText );
  
            // New embedded text file
            currentEmbeddedFileName = line.substring( 7 );
            currentEmbeddedText = '';
  
          } else {
  
            currentEmbeddedText += line + '\n';
  
          }
  
          continue;
  
        }
  
        const lp = new LineParser( line, lineIndex + 1 );
        lp.seekNonSpace();
  
        if ( lp.isAtTheEnd() ) {
  
          // Empty line
          continue;
  
        }
  
        // Parse the line type
        const lineType = lp.getToken();
  
        let material;
        let colorCode;
        let segment;
        let ccw;
        let doubleSided;
        let v0, v1, v2, v3, c0, c1;
  
        switch ( lineType ) {
  
          // Line type 0: Comment or META
          case '0':
  
            // Parse meta directive
            const meta = lp.getToken();
  
            if ( meta ) {
  
              switch ( meta ) {
  
                case '!LDRAW_ORG':
  
                  type = lp.getToken();
                  break;
  
                case '!COLOUR':
  
                  material = loader.parseColorMetaDirective( lp );
                  if ( material ) {
  
                    materials[ material.userData.code ] = material;
  
                  }	else {
  
                    console.warn( 'LDrawLoader: Error parsing material' + lp.getLineNumberString() );
  
                  }
  
                  break;
  
                case '!CATEGORY':
  
                  category = lp.getToken();
                  break;
  
                case '!KEYWORDS':
  
                  const newKeywords = lp.getRemainingString().split( ',' );
                  if ( newKeywords.length > 0 ) {
  
                    if ( ! keywords ) {
  
                      keywords = [];
  
                    }
  
                    newKeywords.forEach( function ( keyword ) {
  
                      keywords.push( keyword.trim() );
  
                    } );
  
                  }
  
                  break;
  
                case 'FILE':
  
                  if ( lineIndex > 0 ) {
  
                    // Start embedded text files parsing
                    parsingEmbeddedFiles = true;
                    currentEmbeddedFileName = lp.getRemainingString();
                    currentEmbeddedText = '';
  
                    bfcCertified = false;
                    bfcCCW = true;
  
                  }
  
                  break;
  
                case 'BFC':
  
                  // Changes to the backface culling state
                  while ( ! lp.isAtTheEnd() ) {
  
                    const token = lp.getToken();
  
                    switch ( token ) {
  
                      case 'CERTIFY':
                      case 'NOCERTIFY':
  
                        bfcCertified = token === 'CERTIFY';
                        bfcCCW = true;
  
                        break;
  
                      case 'CW':
                      case 'CCW':
  
                        bfcCCW = token === 'CCW';
  
                        break;
  
                      case 'INVERTNEXT':
  
                        bfcInverted = true;
  
                        break;
  
                      case 'CLIP':
                      case 'NOCLIP':
  
                        bfcCull = token === 'CLIP';
  
                        break;
  
                      default:
  
                        console.warn( 'THREE.LDrawLoader: BFC directive "' + token + '" is unknown.' );
  
                        break;
  
                    }
  
                  }
  
                  break;
  
                case 'STEP':
  
                  startingBuildingStep = true;
  
                  break;
  
                case 'Author:':
  
                  author = lp.getToken();
  
                  break;
  
                default:
                  // Other meta directives are not implemented
                  break;
  
              }
  
            }
  
            break;
  
            // Line type 1: Sub-object file
          case '1':
  
            colorCode = lp.getToken();
            material = getLocalMaterial( colorCode );
  
            const posX = parseFloat( lp.getToken() );
            const posY = parseFloat( lp.getToken() );
            const posZ = parseFloat( lp.getToken() );
            const m0 = parseFloat( lp.getToken() );
            const m1 = parseFloat( lp.getToken() );
            const m2 = parseFloat( lp.getToken() );
            const m3 = parseFloat( lp.getToken() );
            const m4 = parseFloat( lp.getToken() );
            const m5 = parseFloat( lp.getToken() );
            const m6 = parseFloat( lp.getToken() );
            const m7 = parseFloat( lp.getToken() );
            const m8 = parseFloat( lp.getToken() );
  
            const matrix = new Matrix4().set(
              m0, m1, m2, posX,
              m3, m4, m5, posY,
              m6, m7, m8, posZ,
              0, 0, 0, 1
            );
  
            let fileName = lp.getRemainingString().trim().replace( /\\/g, '/' );
  
            if ( loader.fileMap[ fileName ] ) {
  
              // Found the subobject path in the preloaded file path map
              fileName = loader.fileMap[ fileName ];
  
            } else {
  
              // Standardized subfolders
              if ( fileName.startsWith( 's/' ) ) {
  
                fileName = 'parts/' + fileName;
  
              } else if ( fileName.startsWith( '48/' ) ) {
  
                fileName = 'p/' + fileName;
  
              }
  
            }
  
            subobjects.push( {
              material: material,
              colorCode: colorCode,
              matrix: matrix,
              fileName: fileName,
              inverted: bfcInverted,
              startingBuildingStep: startingBuildingStep
            } );
  
            startingBuildingStep = false;
            bfcInverted = false;
  
            break;
  
            // Line type 2: Line segment
          case '2':
  
            colorCode = lp.getToken();
            material = getLocalMaterial( colorCode );
            v0 = lp.getVector();
            v1 = lp.getVector();
  
            segment = {
              material: material,
              colorCode: colorCode,
              vertices: [ v0, v1 ],
            };
  
            lineSegments.push( segment );
  
            break;
  
            // Line type 5: Conditional Line segment
          case '5':
  
            colorCode = lp.getToken();
            material = getLocalMaterial( colorCode );
            v0 = lp.getVector();
            v1 = lp.getVector();
            c0 = lp.getVector();
            c1 = lp.getVector();
  
            segment = {
              material: material,
              colorCode: colorCode,
              vertices: [ v0, v1 ],
              controlPoints: [ c0, c1 ],
            };
  
            conditionalSegments.push( segment );
  
            break;
  
            // Line type 3: Triangle
          case '3':
  
            colorCode = lp.getToken();
            material = getLocalMaterial( colorCode );
            ccw = bfcCCW;
            doubleSided = ! bfcCertified || ! bfcCull;
  
            if ( ccw === true ) {
  
              v0 = lp.getVector();
              v1 = lp.getVector();
              v2 = lp.getVector();
  
            } else {
  
              v2 = lp.getVector();
              v1 = lp.getVector();
              v0 = lp.getVector();
  
            }
  
            faces.push( {
              material: material,
              colorCode: colorCode,
              faceNormal: null,
              vertices: [ v0, v1, v2 ],
              normals: [ null, null, null ],
            } );
            totalFaces ++;
  
            if ( doubleSided === true ) {
  
              faces.push( {
                material: material,
                colorCode: colorCode,
                faceNormal: null,
                vertices: [ v2, v1, v0 ],
                normals: [ null, null, null ],
              } );
              totalFaces ++;
  
            }
  
            break;
  
            // Line type 4: Quadrilateral
          case '4':
  
            colorCode = lp.getToken();
            material = getLocalMaterial( colorCode );
            ccw = bfcCCW;
            doubleSided = ! bfcCertified || ! bfcCull;
  
            if ( ccw === true ) {
  
              v0 = lp.getVector();
              v1 = lp.getVector();
              v2 = lp.getVector();
              v3 = lp.getVector();
  
            } else {
  
              v3 = lp.getVector();
              v2 = lp.getVector();
              v1 = lp.getVector();
              v0 = lp.getVector();
  
            }
  
            // specifically place the triangle diagonal in the v0 and v1 slots so we can
            // account for the doubling of vertices later when smoothing normals.
            faces.push( {
              material: material,
              colorCode: colorCode,
              faceNormal: null,
              vertices: [ v0, v1, v2, v3 ],
              normals: [ null, null, null, null ],
            } );
            totalFaces += 2;
  
            if ( doubleSided === true ) {
  
              faces.push( {
                material: material,
                colorCode: colorCode,
                faceNormal: null,
                vertices: [ v3, v2, v1, v0 ],
                normals: [ null, null, null, null ],
              } );
              totalFaces += 2;
  
            }
  
            break;
  
          default:
            throw new Error( 'LDrawLoader: Unknown line type "' + lineType + '"' + lp.getLineNumberString() + '.' );
  
        }
  
      }
  
      if ( parsingEmbeddedFiles ) {
  
        this.setData( currentEmbeddedFileName, currentEmbeddedText );
  
      }
  
      return {
        faces,
        conditionalSegments,
        lineSegments,
        type,
        category,
        keywords,
        author,
        subobjects,
        totalFaces,
        startingBuildingStep,
        materials,
        fileName,
        group: null
      };
  
    }
  
    // returns an (optionally cloned) instance of the data
    getData( fileName, clone = true ) {
  
      const key = fileName.toLowerCase();
      const result = this._cache[ key ];
      if ( result === null || result instanceof Promise ) {
  
        return null;
  
      }
  
      if ( clone ) {
  
        return this.cloneResult( result );
  
      } else {
  
        return result;
  
      }
  
    }
  
    // kicks off a fetch and parse of the requested data if it hasn't already been loaded. Returns when
    // the data is ready to use and can be retrieved synchronously with "getData".
    async ensureDataLoaded( fileName ) {
  
      const key = fileName.toLowerCase();
      if ( ! ( key in this._cache ) ) {
  
        // replace the promise with a copy of the parsed data for immediate processing
        this._cache[ key ] = this.fetchData( fileName ).then( text => {
  
          const info = this.parse( text, fileName );
          this._cache[ key ] = info;
          return info;
  
        } );
  
      }
  
      await this._cache[ key ];
  
    }
  
    // sets the data in the cache from parsed data
    setData( fileName, text ) {
  
      const key = fileName.toLowerCase();
      this._cache[ key ] = this.parse( text, fileName );
  
    }
  
  }
  
  // returns the material for an associated color code. If the color code is 16 for a face or 24 for
  // an edge then the passthroughColorCode is used.
  function getMaterialFromCode( colorCode, parentColorCode, materialHierarchy, forEdge ) {
  
    const isPassthrough = ! forEdge && colorCode === MAIN_COLOUR_CODE || forEdge && colorCode === MAIN_EDGE_COLOUR_CODE;
    if ( isPassthrough ) {
  
      colorCode = parentColorCode;
  
    }
  
    return materialHierarchy[ colorCode ] || null;
  
  }
  
  // Class used to parse and build LDraw parts as three.js objects and cache them if they're a "Part" type.
  class LDrawPartsGeometryCache {
  
    constructor( loader ) {
  
      this.loader = loader;
      this.parseCache = new LDrawParsedCache( loader );
      this._cache = {};
  
    }
  
    // Convert the given file information into a mesh by processing subobjects.
    async processIntoMesh( info ) {
  
      const loader = this.loader;
      const parseCache = this.parseCache;
      const faceMaterials = new Set();
  
      // Processes the part subobject information to load child parts and merge geometry onto part
      // piece object.
      const processInfoSubobjects = async ( info, subobject = null ) => {
  
        const subobjects = info.subobjects;
        const promises = [];
  
        // Trigger load of all subobjects. If a subobject isn't a primitive then load it as a separate
        // group which lets instruction steps apply correctly.
        for ( let i = 0, l = subobjects.length; i < l; i ++ ) {
  
          const subobject = subobjects[ i ];
          const promise = parseCache.ensureDataLoaded( subobject.fileName ).then( () => {
  
            const subobjectInfo = parseCache.getData( subobject.fileName, false );
            if ( ! isPrimitiveType( subobjectInfo.type ) ) {
  
              return this.loadModel( subobject.fileName ).catch( error => {
  
                console.warn( error );
                return null;
  
              } );
  
            }
  
            return processInfoSubobjects( parseCache.getData( subobject.fileName ), subobject );
  
          } );
  
          promises.push( promise );
  
        }
  
        const group = new Group();
        group.userData.category = info.category;
        group.userData.keywords = info.keywords;
        group.userData.author = info.author;
        group.userData.type = info.type;
        group.userData.fileName = info.fileName;
        info.group = group;
  
        const subobjectInfos = await Promise.all( promises );
        for ( let i = 0, l = subobjectInfos.length; i < l; i ++ ) {
  
          const subobject = info.subobjects[ i ];
          const subobjectInfo = subobjectInfos[ i ];
  
          if ( subobjectInfo === null ) {
  
            // the subobject failed to load
            continue;
  
          }
  
          // if the subobject was loaded as a separate group then apply the parent scopes materials
          if ( subobjectInfo.isGroup ) {
  
            const subobjectGroup = subobjectInfo;
            subobject.matrix.decompose( subobjectGroup.position, subobjectGroup.quaternion, subobjectGroup.scale );
            subobjectGroup.userData.startingBuildingStep = subobject.startingBuildingStep;
            subobjectGroup.name = subobject.fileName;
  
            loader.applyMaterialsToMesh( subobjectGroup, subobject.colorCode, info.materials );
            subobjectGroup.userData.colorCode = subobject.colorCode;
  
            group.add( subobjectGroup );
            continue;
  
          }
  
          // add the subobject group if it has children in case it has both children and primitives
          if ( subobjectInfo.group.children.length ) {
  
            group.add( subobjectInfo.group );
  
          }
  
          // transform the primitives into the local space of the parent piece and append them to
          // to the parent primitives list.
          const parentLineSegments = info.lineSegments;
          const parentConditionalSegments = info.conditionalSegments;
          const parentFaces = info.faces;
  
          const lineSegments = subobjectInfo.lineSegments;
          const conditionalSegments = subobjectInfo.conditionalSegments;
  
          const faces = subobjectInfo.faces;
          const matrix = subobject.matrix;
          const inverted = subobject.inverted;
          const matrixScaleInverted = matrix.determinant() < 0;
          const colorCode = subobject.colorCode;
  
          const lineColorCode = colorCode === MAIN_COLOUR_CODE ? MAIN_EDGE_COLOUR_CODE : colorCode;
          for ( let i = 0, l = lineSegments.length; i < l; i ++ ) {
  
            const ls = lineSegments[ i ];
            const vertices = ls.vertices;
            vertices[ 0 ].applyMatrix4( matrix );
            vertices[ 1 ].applyMatrix4( matrix );
            ls.colorCode = ls.colorCode === MAIN_EDGE_COLOUR_CODE ? lineColorCode : ls.colorCode;
            ls.material = ls.material || getMaterialFromCode( ls.colorCode, ls.colorCode, info.materials, true );
  
            parentLineSegments.push( ls );
  
          }
  
          for ( let i = 0, l = conditionalSegments.length; i < l; i ++ ) {
  
            const os = conditionalSegments[ i ];
            const vertices = os.vertices;
            const controlPoints = os.controlPoints;
            vertices[ 0 ].applyMatrix4( matrix );
            vertices[ 1 ].applyMatrix4( matrix );
            controlPoints[ 0 ].applyMatrix4( matrix );
            controlPoints[ 1 ].applyMatrix4( matrix );
            os.colorCode = os.colorCode === MAIN_EDGE_COLOUR_CODE ? lineColorCode : os.colorCode;
            os.material = os.material || getMaterialFromCode( os.colorCode, os.colorCode, info.materials, true );
  
            parentConditionalSegments.push( os );
  
          }
  
          for ( let i = 0, l = faces.length; i < l; i ++ ) {
  
            const tri = faces[ i ];
            const vertices = tri.vertices;
            for ( let i = 0, l = vertices.length; i < l; i ++ ) {
  
              vertices[ i ].applyMatrix4( matrix );
  
            }
  
            tri.colorCode = tri.colorCode === MAIN_COLOUR_CODE ? colorCode : tri.colorCode;
            tri.material = tri.material || getMaterialFromCode( tri.colorCode, colorCode, info.materials, false );
            faceMaterials.add( tri.colorCode );
  
            // If the scale of the object is negated then the triangle winding order
            // needs to be flipped.
            if ( matrixScaleInverted !== inverted ) {
  
              vertices.reverse();
  
            }
  
            parentFaces.push( tri );
  
          }
  
          info.totalFaces += subobjectInfo.totalFaces;
  
        }
  
        // Apply the parent subobjects pass through material code to this object. This is done several times due
        // to material scoping.
        if ( subobject ) {
  
          loader.applyMaterialsToMesh( group, subobject.colorCode, info.materials );
          group.userData.colorCode = subobject.colorCode;
  
        }
  
        return info;
  
      };
  
      // Track material use to see if we need to use the normal smooth slow path for hard edges.
      for ( let i = 0, l = info.faces; i < l; i ++ ) {
  
        faceMaterials.add( info.faces[ i ].colorCode );
  
      }
  
      await processInfoSubobjects( info );
  
      if ( loader.smoothNormals ) {
  
        const checkSubSegments = faceMaterials.size > 1;
        generateFaceNormals( info.faces );
        smoothNormals( info.faces, info.lineSegments, checkSubSegments );
  
      }
  
      // Add the primitive objects and metadata.
      const group = info.group;
      if ( info.faces.length > 0 ) {
  
        group.add( createObject( this.loader, info.faces, 3, false, info.totalFaces ) );
  
      }
  
      if ( info.lineSegments.length > 0 ) {
  
        group.add( createObject( this.loader, info.lineSegments, 2 ) );
  
      }
  
      if ( info.conditionalSegments.length > 0 ) {
  
        group.add( createObject( this.loader, info.conditionalSegments, 2, true ) );
  
      }
  
      return group;
  
    }
  
    hasCachedModel( fileName ) {
  
      return fileName !== null && fileName.toLowerCase() in this._cache;
  
    }
  
    async getCachedModel( fileName ) {
  
      if ( fileName !== null && this.hasCachedModel( fileName ) ) {
  
        const key = fileName.toLowerCase();
        const group = await this._cache[ key ];
        return group.clone();
  
      } else {
  
        return null;
  
      }
  
    }
  
    // Loads and parses the model with the given file name. Returns a cached copy if available.
    async loadModel( fileName ) {
  
      const parseCache = this.parseCache;
      const key = fileName.toLowerCase();
      if ( this.hasCachedModel( fileName ) ) {
  
        // Return cached model if available.
        return this.getCachedModel( fileName );
  
      } else {
  
        // Otherwise parse a new model.
        // Ensure the file data is loaded and pre parsed.
        await parseCache.ensureDataLoaded( fileName );
  
        const info = parseCache.getData( fileName );
        const promise = this.processIntoMesh( info );
  
        // Now that the file has loaded it's possible that another part parse has been waiting in parallel
        // so check the cache again to see if it's been added since the last async operation so we don't
        // do unnecessary work.
        if ( this.hasCachedModel( fileName ) ) {
  
          return this.getCachedModel( fileName );
  
        }
  
        // Cache object if it's a part so it can be reused later.
        if ( isPartType( info.type ) ) {
  
          this._cache[ key ] = promise;
  
        }
  
        // return a copy
        const group = await promise;
        return group.clone();
  
      }
  
    }
  
    // parses the given model text into a renderable object. Returns cached copy if available.
    async parseModel( text ) {
  
      const parseCache = this.parseCache;
      const info = parseCache.parse( text );
      if ( isPartType( info.type ) && this.hasCachedModel( info.fileName ) ) {
  
        return this.getCachedModel( info.fileName );
  
      }
  
      return this.processIntoMesh( info );
  
    }
  
  }
  
  function sortByMaterial( a, b ) {
  
    if ( a.colorCode === b.colorCode ) {
  
      return 0;
  
    }
  
    if ( a.colorCode < b.colorCode ) {
  
      return - 1;
  
    }
  
    return 1;
  
  }
  
  function createObject( loader, elements, elementSize, isConditionalSegments = false, totalElements = null ) {
  
    // Creates a LineSegments (elementSize = 2) or a Mesh (elementSize = 3 )
    // With per face / segment material, implemented with mesh groups and materials array
  
    // Sort the faces or line segments by color code to make later the mesh groups
    elements.sort( sortByMaterial );
  
    if ( totalElements === null ) {
  
      totalElements = elements.length;
  
    }
  
    const positions = new Float32Array( elementSize * totalElements * 3 );
    const normals = elementSize === 3 ? new Float32Array( elementSize * totalElements * 3 ) : null;
    const materials = [];
  
    const quadArray = new Array( 6 );
    const bufferGeometry = new BufferGeometry();
    let prevMaterial = null;
    let index0 = 0;
    let numGroupVerts = 0;
    let offset = 0;
  
    for ( let iElem = 0, nElem = elements.length; iElem < nElem; iElem ++ ) {
  
      const elem = elements[ iElem ];
      let vertices = elem.vertices;
      if ( vertices.length === 4 ) {
  
        quadArray[ 0 ] = vertices[ 0 ];
        quadArray[ 1 ] = vertices[ 1 ];
        quadArray[ 2 ] = vertices[ 2 ];
        quadArray[ 3 ] = vertices[ 0 ];
        quadArray[ 4 ] = vertices[ 2 ];
        quadArray[ 5 ] = vertices[ 3 ];
        vertices = quadArray;
  
      }
  
      for ( let j = 0, l = vertices.length; j < l; j ++ ) {
  
        const v = vertices[ j ];
        const index = offset + j * 3;
        positions[ index + 0 ] = v.x;
        positions[ index + 1 ] = v.y;
        positions[ index + 2 ] = v.z;
  
      }
  
      // create the normals array if this is a set of faces
      if ( elementSize === 3 ) {
  
        if ( ! elem.faceNormal ) {
  
          const v0 = vertices[ 0 ];
          const v1 = vertices[ 1 ];
          const v2 = vertices[ 2 ];
          _tempVec0.subVectors( v1, v0 );
          _tempVec1.subVectors( v2, v1 );
          elem.faceNormal = new Vector3()
            .crossVectors( _tempVec0, _tempVec1 )
            .normalize();
  
        }
  
        let elemNormals = elem.normals;
        if ( elemNormals.length === 4 ) {
  
          quadArray[ 0 ] = elemNormals[ 0 ];
          quadArray[ 1 ] = elemNormals[ 1 ];
          quadArray[ 2 ] = elemNormals[ 2 ];
          quadArray[ 3 ] = elemNormals[ 0 ];
          quadArray[ 4 ] = elemNormals[ 2 ];
          quadArray[ 5 ] = elemNormals[ 3 ];
          elemNormals = quadArray;
  
        }
  
        for ( let j = 0, l = elemNormals.length; j < l; j ++ ) {
  
          // use face normal if a vertex normal is not provided
          let n = elem.faceNormal;
          if ( elemNormals[ j ] ) {
  
            n = elemNormals[ j ].norm;
  
          }
  
          const index = offset + j * 3;
          normals[ index + 0 ] = n.x;
          normals[ index + 1 ] = n.y;
          normals[ index + 2 ] = n.z;
  
        }
  
      }
  
      if ( prevMaterial !== elem.colorCode ) {
  
        if ( prevMaterial !== null ) {
  
          bufferGeometry.addGroup( index0, numGroupVerts, materials.length - 1 );
  
        }
  
        const material = elem.material;
  
        if ( material !== null ) {
  
          if ( elementSize === 3 ) {
  
            materials.push( material );
  
          } else if ( elementSize === 2 ) {
  
            if ( isConditionalSegments ) {
  
              const edgeMaterial = loader.edgeMaterialCache.get( material );
  
              materials.push( loader.conditionalEdgeMaterialCache.get( edgeMaterial ) );
  
            } else {
  
              materials.push( loader.edgeMaterialCache.get( material ) );
  
            }
  
          }
  
        } else {
  
          // If a material has not been made available yet then keep the color code string in the material array
          // to save the spot for the material once a parent scopes materials are being applied to the object.
          materials.push( elem.colorCode );
  
        }
  
        prevMaterial = elem.colorCode;
        index0 = offset / 3;
        numGroupVerts = vertices.length;
  
      } else {
  
        numGroupVerts += vertices.length;
  
      }
  
      offset += 3 * vertices.length;
  
    }
  
    if ( numGroupVerts > 0 ) {
  
      bufferGeometry.addGroup( index0, Infinity, materials.length - 1 );
  
    }
  
    bufferGeometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) );
  
    if ( normals !== null ) {
  
      bufferGeometry.setAttribute( 'normal', new BufferAttribute( normals, 3 ) );
  
    }
  
    let object3d = null;
  
    if ( elementSize === 2 ) {
  
      if ( isConditionalSegments ) {
  
        object3d = new ConditionalLineSegments( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials );
  
      } else {
  
        object3d = new LineSegments( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials );
  
      }
  
    } else if ( elementSize === 3 ) {
  
      object3d = new Mesh( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials );
  
    }
  
    if ( isConditionalSegments ) {
  
      object3d.isConditionalLine = true;
  
      const controlArray0 = new Float32Array( elements.length * 3 * 2 );
      const controlArray1 = new Float32Array( elements.length * 3 * 2 );
      const directionArray = new Float32Array( elements.length * 3 * 2 );
      for ( let i = 0, l = elements.length; i < l; i ++ ) {
  
        const os = elements[ i ];
        const vertices = os.vertices;
        const controlPoints = os.controlPoints;
        const c0 = controlPoints[ 0 ];
        const c1 = controlPoints[ 1 ];
        const v0 = vertices[ 0 ];
        const v1 = vertices[ 1 ];
        const index = i * 3 * 2;
        controlArray0[ index + 0 ] = c0.x;
        controlArray0[ index + 1 ] = c0.y;
        controlArray0[ index + 2 ] = c0.z;
        controlArray0[ index + 3 ] = c0.x;
        controlArray0[ index + 4 ] = c0.y;
        controlArray0[ index + 5 ] = c0.z;
  
        controlArray1[ index + 0 ] = c1.x;
        controlArray1[ index + 1 ] = c1.y;
        controlArray1[ index + 2 ] = c1.z;
        controlArray1[ index + 3 ] = c1.x;
        controlArray1[ index + 4 ] = c1.y;
        controlArray1[ index + 5 ] = c1.z;
  
        directionArray[ index + 0 ] = v1.x - v0.x;
        directionArray[ index + 1 ] = v1.y - v0.y;
        directionArray[ index + 2 ] = v1.z - v0.z;
        directionArray[ index + 3 ] = v1.x - v0.x;
        directionArray[ index + 4 ] = v1.y - v0.y;
        directionArray[ index + 5 ] = v1.z - v0.z;
  
      }
  
      bufferGeometry.setAttribute( 'control0', new BufferAttribute( controlArray0, 3, false ) );
      bufferGeometry.setAttribute( 'control1', new BufferAttribute( controlArray1, 3, false ) );
      bufferGeometry.setAttribute( 'direction', new BufferAttribute( directionArray, 3, false ) );
  
    }
  
    return object3d;
  
  }
  
  //
  
  class LDrawLoader extends Loader {
  
    constructor( manager ) {
  
      super( manager );
  
      // Array of THREE.Material
      this.materials = [];
      this.materialLibrary = {};
      this.edgeMaterialCache = new WeakMap();
      this.conditionalEdgeMaterialCache = new WeakMap();
  
      // This also allows to handle the embedded text files ("0 FILE" lines)
      this.partsCache = new LDrawPartsGeometryCache( this );
  
      // This object is a map from file names to paths. It agilizes the paths search. If it is not set then files will be searched by trial and error.
      this.fileMap = {};
  
      // Initializes the materials library with default materials
      this.setMaterials( [] );
  
      // If this flag is set to true the vertex normals will be smoothed.
      this.smoothNormals = true;
  
      // The path to load parts from the LDraw parts library from.
      this.partsLibraryPath = '';
  
      // Material assigned to not available colors for meshes and edges
      this.missingColorMaterial = new MeshStandardMaterial( { name: Loader.DEFAULT_MATERIAL_NAME, color: 0xFF00FF, roughness: 0.3, metalness: 0 } );
      this.missingEdgeColorMaterial = new LineBasicMaterial( { name: Loader.DEFAULT_MATERIAL_NAME, color: 0xFF00FF } );
      this.missingConditionalEdgeColorMaterial = new LDrawConditionalLineMaterial( { name: Loader.DEFAULT_MATERIAL_NAME, fog: true, color: 0xFF00FF } );
      this.edgeMaterialCache.set( this.missingColorMaterial, this.missingEdgeColorMaterial );
      this.conditionalEdgeMaterialCache.set( this.missingEdgeColorMaterial, this.missingConditionalEdgeColorMaterial );
  
    }
  
    setPartsLibraryPath( path ) {
  
      this.partsLibraryPath = path;
      return this;
  
    }
  
    async preloadMaterials( url ) {
  
      const fileLoader = new FileLoader( this.manager );
      fileLoader.setPath( this.path );
      fileLoader.setRequestHeader( this.requestHeader );
      fileLoader.setWithCredentials( this.withCredentials );
  
      const text = await fileLoader.loadAsync( url );
      const colorLineRegex = /^0 !COLOUR/;
      const lines = text.split( /[\n\r]/g );
      const materials = [];
      for ( let i = 0, l = lines.length; i < l; i ++ ) {
  
        const line = lines[ i ];
        if ( colorLineRegex.test( line ) ) {
  
          const directive = line.replace( colorLineRegex, '' );
          const material = this.parseColorMetaDirective( new LineParser( directive ) );
          materials.push( material );
  
        }
  
      }
  
      this.setMaterials( materials );
  
    }
  
    load( url, onLoad, onProgress, onError ) {
  
      const fileLoader = new FileLoader( this.manager );
      fileLoader.setPath( this.path );
      fileLoader.setRequestHeader( this.requestHeader );
      fileLoader.setWithCredentials( this.withCredentials );
      fileLoader.load( url, text => {
  
        this.partsCache
          .parseModel( text, this.materialLibrary )
          .then( group => {
  
            this.applyMaterialsToMesh( group, MAIN_COLOUR_CODE, this.materialLibrary, true );
            this.computeBuildingSteps( group );
            group.userData.fileName = url;
            onLoad( group );
  
          } )
          .catch( onError );
  
      }, onProgress, onError );
  
    }
  
    parse( text, onLoad ) {
  
      this.partsCache
        .parseModel( text, this.materialLibrary )
        .then( group => {
  
          this.applyMaterialsToMesh( group, MAIN_COLOUR_CODE, this.materialLibrary, true );
          this.computeBuildingSteps( group );
          group.userData.fileName = '';
          onLoad( group );
  
        } );
  
    }
  
    setMaterials( materials ) {
  
      this.materialLibrary = {};
      this.materials = [];
      for ( let i = 0, l = materials.length; i < l; i ++ ) {
  
        this.addMaterial( materials[ i ] );
  
      }
  
      // Add default main triangle and line edge materials (used in pieces that can be colored with a main color)
      this.addMaterial( this.parseColorMetaDirective( new LineParser( 'Main_Colour CODE 16 VALUE #FF8080 EDGE #333333' ) ) );
      this.addMaterial( this.parseColorMetaDirective( new LineParser( 'Edge_Colour CODE 24 VALUE #A0A0A0 EDGE #333333' ) ) );
  
      return this;
  
    }
  
    setFileMap( fileMap ) {
  
      this.fileMap = fileMap;
  
      return this;
  
    }
  
    addMaterial( material ) {
  
      // Adds a material to the material library which is on top of the parse scopes stack. And also to the materials array
  
      const matLib = this.materialLibrary;
      if ( ! matLib[ material.userData.code ] ) {
  
        this.materials.push( material );
        matLib[ material.userData.code ] = material;
  
      }
  
      return this;
  
    }
  
    getMaterial( colorCode ) {
  
      if ( colorCode.startsWith( '0x2' ) ) {
  
        // Special 'direct' material value (RGB color)
        const color = colorCode.substring( 3 );
  
        return this.parseColorMetaDirective( new LineParser( 'Direct_Color_' + color + ' CODE -1 VALUE #' + color + ' EDGE #' + color + '' ) );
  
      }
  
      return this.materialLibrary[ colorCode ] || null;
  
    }
  
    // Applies the appropriate materials to a prebuilt hierarchy of geometry. Assumes that color codes are present
    // in the material array if they need to be filled in.
    applyMaterialsToMesh( group, parentColorCode, materialHierarchy, finalMaterialPass = false ) {
  
      // find any missing materials as indicated by a color code string and replace it with a material from the current material lib
      const loader = this;
      const parentIsPassthrough = parentColorCode === MAIN_COLOUR_CODE;
      group.traverse( c => {
  
        if ( c.isMesh || c.isLineSegments ) {
  
          if ( Array.isArray( c.material ) ) {
  
            for ( let i = 0, l = c.material.length; i < l; i ++ ) {
  
              if ( ! c.material[ i ].isMaterial ) {
  
                c.material[ i ] = getMaterial( c, c.material[ i ] );
  
              }
  
            }
  
          } else if ( ! c.material.isMaterial ) {
  
            c.material = getMaterial( c, c.material );
  
          }
  
        }
  
      } );
  
  
      // Returns the appropriate material for the object (line or face) given color code. If the code is "pass through"
      // (24 for lines, 16 for edges) then the pass through color code is used. If that is also pass through then it's
      // simply returned for the subsequent material application.
      function getMaterial( c, colorCode ) {
  
        // if our parent is a passthrough color code and we don't have the current material color available then
        // return early.
        if ( parentIsPassthrough && ! ( colorCode in materialHierarchy ) && ! finalMaterialPass ) {
  
          return colorCode;
  
        }
  
        const forEdge = c.isLineSegments || c.isConditionalLine;
        const isPassthrough = ! forEdge && colorCode === MAIN_COLOUR_CODE || forEdge && colorCode === MAIN_EDGE_COLOUR_CODE;
        if ( isPassthrough ) {
  
          colorCode = parentColorCode;
  
        }
  
        let material = null;
        if ( colorCode in materialHierarchy ) {
  
          material = materialHierarchy[ colorCode ];
  
        } else if ( finalMaterialPass ) {
  
          // see if we can get the final material from from the "getMaterial" function which will attempt to
          // parse the "direct" colors
          material = loader.getMaterial( colorCode );
          if ( material === null ) {
  
            // otherwise throw a warning if this is final opportunity to set the material
            console.warn( `LDrawLoader: Material properties for code ${ colorCode } not available.` );
  
            // And return the 'missing color' material
            material = loader.missingColorMaterial;
  
          }
  
  
        } else {
  
          return colorCode;
  
        }
  
        if ( c.isLineSegments ) {
  
          material = loader.edgeMaterialCache.get( material );
  
          if ( c.isConditionalLine ) {
  
            material = loader.conditionalEdgeMaterialCache.get( material );
  
          }
  
        }
  
        return material;
  
      }
  
    }
  
    getMainMaterial() {
  
      return this.getMaterial( MAIN_COLOUR_CODE );
  
    }
  
    getMainEdgeMaterial() {
  
      const mat = this.getMaterial( MAIN_EDGE_COLOUR_CODE );
      return mat ? this.edgeMaterialCache.get( mat ) : null;
  
    }
  
    parseColorMetaDirective( lineParser ) {
  
      // Parses a color definition and returns a THREE.Material
  
      let code = null;
  
      // Triangle and line colors
      let fillColor = '#FF00FF';
      let edgeColor = '#FF00FF';
  
      // Transparency
      let alpha = 1;
      let isTransparent = false;
      // Self-illumination:
      let luminance = 0;
  
      let finishType = FINISH_TYPE_DEFAULT;
  
      let edgeMaterial = null;
  
      const name = lineParser.getToken();
      if ( ! name ) {
  
        throw new Error( 'LDrawLoader: Material name was expected after "!COLOUR tag' + lineParser.getLineNumberString() + '.' );
  
      }
  
      // Parse tag tokens and their parameters
      let token = null;
      while ( true ) {
  
        token = lineParser.getToken();
  
        if ( ! token ) {
  
          break;
  
        }
  
        if ( ! parseLuminance( token ) ) {
  
          switch ( token.toUpperCase() ) {
  
            case 'CODE':
  
              code = lineParser.getToken();
              break;
  
            case 'VALUE':
  
              fillColor = lineParser.getToken();
              if ( fillColor.startsWith( '0x' ) ) {
  
                fillColor = '#' + fillColor.substring( 2 );
  
              } else if ( ! fillColor.startsWith( '#' ) ) {
  
                throw new Error( 'LDrawLoader: Invalid color while parsing material' + lineParser.getLineNumberString() + '.' );
  
              }
  
              break;
  
            case 'EDGE':
  
              edgeColor = lineParser.getToken();
              if ( edgeColor.startsWith( '0x' ) ) {
  
                edgeColor = '#' + edgeColor.substring( 2 );
  
              } else if ( ! edgeColor.startsWith( '#' ) ) {
  
                // Try to see if edge color is a color code
                edgeMaterial = this.getMaterial( edgeColor );
                if ( ! edgeMaterial ) {
  
                  throw new Error( 'LDrawLoader: Invalid edge color while parsing material' + lineParser.getLineNumberString() + '.' );
  
                }
  
                // Get the edge material for this triangle material
                edgeMaterial = this.edgeMaterialCache.get( edgeMaterial );
  
              }
  
              break;
  
            case 'ALPHA':
  
              alpha = parseInt( lineParser.getToken() );
  
              if ( isNaN( alpha ) ) {
  
                throw new Error( 'LDrawLoader: Invalid alpha value in material definition' + lineParser.getLineNumberString() + '.' );
  
              }
  
              alpha = Math.max( 0, Math.min( 1, alpha / 255 ) );
  
              if ( alpha < 1 ) {
  
                isTransparent = true;
  
              }
  
              break;
  
            case 'LUMINANCE':
  
              if ( ! parseLuminance( lineParser.getToken() ) ) {
  
                throw new Error( 'LDrawLoader: Invalid luminance value in material definition' + LineParser.getLineNumberString() + '.' );
  
              }
  
              break;
  
            case 'CHROME':
              finishType = FINISH_TYPE_CHROME;
              break;
  
            case 'PEARLESCENT':
              finishType = FINISH_TYPE_PEARLESCENT;
              break;
  
            case 'RUBBER':
              finishType = FINISH_TYPE_RUBBER;
              break;
  
            case 'MATTE_METALLIC':
              finishType = FINISH_TYPE_MATTE_METALLIC;
              break;
  
            case 'METAL':
              finishType = FINISH_TYPE_METAL;
              break;
  
            case 'MATERIAL':
              // Not implemented
              lineParser.setToEnd();
              break;
  
            default:
              throw new Error( 'LDrawLoader: Unknown token "' + token + '" while parsing material' + lineParser.getLineNumberString() + '.' );
  
          }
  
        }
  
      }
  
      let material = null;
  
      switch ( finishType ) {
  
        case FINISH_TYPE_DEFAULT:
  
          material = new MeshStandardMaterial( { roughness: 0.3, metalness: 0 } );
          break;
  
        case FINISH_TYPE_PEARLESCENT:
  
          // Try to imitate pearlescency by making the surface glossy
          material = new MeshStandardMaterial( { roughness: 0.3, metalness: 0.25 } );
          break;
  
        case FINISH_TYPE_CHROME:
  
          // Mirror finish surface
          material = new MeshStandardMaterial( { roughness: 0, metalness: 1 } );
          break;
  
        case FINISH_TYPE_RUBBER:
  
          // Rubber finish
          material = new MeshStandardMaterial( { roughness: 0.9, metalness: 0 } );
          break;
  
        case FINISH_TYPE_MATTE_METALLIC:
  
          // Brushed metal finish
          material = new MeshStandardMaterial( { roughness: 0.8, metalness: 0.4 } );
          break;
  
        case FINISH_TYPE_METAL:
  
          // Average metal finish
          material = new MeshStandardMaterial( { roughness: 0.2, metalness: 0.85 } );
          break;
  
        default:
          // Should not happen
          break;
  
      }
  
      material.color.setStyle( fillColor, COLOR_SPACE_LDRAW );
      material.transparent = isTransparent;
      material.premultipliedAlpha = true;
      material.opacity = alpha;
      material.depthWrite = ! isTransparent;
  
      material.polygonOffset = true;
      material.polygonOffsetFactor = 1;
  
      if ( luminance !== 0 ) {
  
        material.emissive.setStyle( fillColor, COLOR_SPACE_LDRAW ).multiplyScalar( luminance );
  
      }
  
      if ( ! edgeMaterial ) {
  
        // This is the material used for edges
        edgeMaterial = new LineBasicMaterial( {
          color: new Color().setStyle( edgeColor, COLOR_SPACE_LDRAW ),
          transparent: isTransparent,
          opacity: alpha,
          depthWrite: ! isTransparent
        } );
        edgeMaterial.color;
        edgeMaterial.userData.code = code;
        edgeMaterial.name = name + ' - Edge';
  
        // This is the material used for conditional edges
        const conditionalEdgeMaterial = new LDrawConditionalLineMaterial( {
  
          fog: true,
          transparent: isTransparent,
          depthWrite: ! isTransparent,
          color: new Color().setStyle( edgeColor, COLOR_SPACE_LDRAW ),
          opacity: alpha,
  
        } );
        conditionalEdgeMaterial.userData.code = code;
        conditionalEdgeMaterial.name = name + ' - Conditional Edge';
  
        this.conditionalEdgeMaterialCache.set( edgeMaterial, conditionalEdgeMaterial );
  
      }
  
      material.userData.code = code;
      material.name = name;
  
      this.edgeMaterialCache.set( material, edgeMaterial );
  
      this.addMaterial( material );
  
      return material;
  
      function parseLuminance( token ) {
  
        // Returns success
  
        let lum;
  
        if ( token.startsWith( 'LUMINANCE' ) ) {
  
          lum = parseInt( token.substring( 9 ) );
  
        } else {
  
          lum = parseInt( token );
  
        }
  
        if ( isNaN( lum ) ) {
  
          return false;
  
        }
  
        luminance = Math.max( 0, Math.min( 1, lum / 255 ) );
  
        return true;
  
      }
  
    }
  
    computeBuildingSteps( model ) {
  
      // Sets userdata.buildingStep number in Group objects and userData.numBuildingSteps number in the root Group object.
  
      let stepNumber = 0;
  
      model.traverse( c => {
  
        if ( c.isGroup ) {
  
          if ( c.userData.startingBuildingStep ) {
  
            stepNumber ++;
  
          }
  
          c.userData.buildingStep = stepNumber;
  
        }
  
      } );
  
      model.userData.numBuildingSteps = stepNumber + 1;
  
    }
  
  }
  
  export { LDrawLoader };
  